/**********************************************************************
 *   Copyright: (C)2024 LingYun IoT System Studio
 *      Author: LiJiahui<2199250859@qq.com>
 *
 * Description: The purpose of this code is to provide a simple C library,
 *              which providing software bit-bang of the I2C protocol on
 *              any GPIO pins for ISKBoard.
 *
 *   ChangeLog:
 *        Version    Date       Author            Description
 *        V1.0.0  2024.08.29    LiJiahui      Release initial version
 *
 ***********************************************************************/

#include "i2c_bitbang.h"
#include "miscdev.h"  /* udelay() implement by TIM6 */

/* I2C GPIO port level functions */

#define mdelay(x) HAL_Delay(x)

enum
{
	LOW,
	HIGH,
};

i2c_bus_t	i2c_bus =
{
	/*  Addr     SCL_Pin               SDA_Pin     */
		0x00, {GPIOB, GPIO_PIN_6}, {GPIOB, GPIO_PIN_7},
};

/*------------------------------------------------------------------------------
 * 这段代码使用推挽输出是为了优化速度（减少上升时间），适用于 单主设备、短距离通信
 * 如果严格遵循 I2C 标准（多主设备、长距离），需使用 开漏输出 + 外部上拉
 * SCL 用推挽没问题，因为主设备完全控制时钟，从设备只能拉低
 * SDA 动态切换（推挽输出 + 输入上拉），既保证发送速度，又能接收数据
 * -----------------------------------------------------------------------------
 */


/* 读取数据（从设备->主设备） */
static inline void SDA_IN(void)
{
	GPIO_InitTypeDef GPIO_InitStruct = {0};

	GPIO_InitStruct.Pin = i2c_bus.sda.pin;
	GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
	GPIO_InitStruct.Pull = GPIO_PULLUP; /* I2C 协议要求 SDA 和 SCL 必须上拉，避免总线悬空导致误触发 */
	GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
	HAL_GPIO_Init(i2c_bus.sda.group, &GPIO_InitStruct);
}

/* 发送数据（主设备->从设备） */
static inline void SDA_OUT(void)
{
	GPIO_InitTypeDef GPIO_InitStruct = {0};

	GPIO_InitStruct.Pin = i2c_bus.sda.pin;
	GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
	GPIO_InitStruct.Pull = GPIO_PULLUP; /* I2C 协议要求 SDA 和 SCL 必须上拉，避免总线悬空导致误触发 */
	GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
	HAL_GPIO_Init(i2c_bus.sda.group, &GPIO_InitStruct);
}

static inline void SCL_OUT(void)
{
	GPIO_InitTypeDef GPIO_InitStruct = {0};

	GPIO_InitStruct.Pin = i2c_bus.scl.pin;
	GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
	GPIO_InitStruct.Pull = GPIO_PULLUP; /* I2C 协议要求 SDA 和 SCL 必须上拉，避免总线悬空导致误触发 */
	GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
	HAL_GPIO_Init(i2c_bus.scl.group, &GPIO_InitStruct);
}

static inline void SCL_SET(int level)
{
	HAL_GPIO_WritePin(i2c_bus.scl.group, i2c_bus.scl.pin, level?GPIO_PIN_SET:GPIO_PIN_RESET);
}

static inline void SDA_SET(int level)
{
	HAL_GPIO_WritePin(i2c_bus.sda.group, i2c_bus.sda.pin, level?GPIO_PIN_SET:GPIO_PIN_RESET);
}

static inline GPIO_PinState SCL_GET(void)
{
	return HAL_GPIO_ReadPin(i2c_bus.scl.group, i2c_bus.scl.pin);
}

static inline GPIO_PinState SDA_GET(void)
{
	return HAL_GPIO_ReadPin(i2c_bus.sda.group, i2c_bus.sda.pin);
}


/* I2C Low level API functions */

/* 发送I2C 起始条件（START）
 * 时序：1、SDA从高->低（SCL为高电平）； 2、通知从设备“通信开始”
 */
void I2C_StartCondition(void)
{
	SDA_OUT();
	SCL_OUT();

	/* 1.首先确保SDA和SCL都为高 */
	SDA_SET(HIGH);
	SCL_SET(HIGH);
	udelay(1); /* 确保总线空闲 */

	/* 2. 产生起始条件：SDA从高变低 */
	SDA_SET(LOW);
	udelay(1); /* t_HD; STA-保持时间 */

	/* 3. 拉低SCL完成起始条件 */
	SCL_SET(LOW);
	udelay(1);
}


/* 发送I2C停止条件（STOP）
 *时序： 1、SDA从低->高（SCL为高电平）2、通知从设备“通信结束”
 */
void I2C_StopCondition(void)
{
	SDA_OUT(); /* SCL在I2C_StartCondition就设置为了输出模式，无需变换模式，整个通信过程中都不会切换模式 */

	SCL_SET(LOW);
	SDA_SET(LOW);
	udelay(1);

	SCL_SET(HIGH);
	SDA_SET(HIGH);
	udelay(1);
}


/* 发送设备地址 + 读写方向（7位地址 + 1位读写标志）写：0 读：1 */
int I2C_SendAddress(uint8_t dir)
{
	uint8_t	addr;
	uint8_t rv;

	/* 根据传入的dir判断进行 读/写 操作 */
	if( I2C_WR == dir )
		addr = W_ADDR(i2c_bus.addr);
	else
		addr = R_ADDR(i2c_bus.addr);

	rv = I2C_WriteByte(addr);

	return rv;
}


/* 发送ACK(应答)或NAK(非应答)
 *---------------------------------------------------------------------------------------------------------
 * ACK:SDA为低电平，表示“数据已接受，请继续”
 * NAK：SDA为高电平，表示停止发送
 * 在 I2C 协议中，ACK（应答）和 NAK（非应答）的时序要求 SCL 在低电平时设置 SDA，再产生一个高脉冲
 * ACK/NAK 的传输必须发生在 SCL 的第 9 个时钟周期
 * 时序：1：SCL为低电平时：主设备设置SDA电平（ACK=低，NAK=高） 2：SCL为高电平：从设备在SCL高电平期间读取SDA
 * 3：SCL再次拉低：完成ACK/NAK周期，准备后续数据传输
 * ---------------------------------------------------------------------------------------------------------
 */
void I2C_Ack(uint8_t ack)
{
	if( ACK_NONE == ack )
		return ;


	SCL_SET(LOW);
	SDA_OUT();

	if( ACK == ack )
		SDA_SET(LOW);
	else if( NAK == ack )
		SDA_SET(HIGH);

	udelay(1);

	SCL_SET(HIGH);
	udelay(1);
	SCL_SET(LOW);
}


/* 主设备只能在 SCL 为低电平时改变 SDA 的值,避免意外触发start/stop条件，确保从设备在 SCL 高电平时能稳定采样数据
 *当SCL为高电平是，SDA上的数据必须保持稳定
 */
int I2C_WriteByte(uint8_t txByte)
{
	int		error = 0;
	uint8_t	mask;

	SDA_OUT();
	SCL_SET(LOW);

	/* Start send 8 bits data by MSB, 根据掩码mask获取每一位（从高到低） */
	for(mask=0x80; mask>0; mask>>=1)
	{
		if((mask & txByte) == 0)
			SDA_SET(LOW);
		else
			SDA_SET(HIGH);

		udelay(1); // data set-up time (t_SU;DAT)
		SCL_SET(HIGH); //从设备采样SDA
		udelay(1);  // SCL high time (t_HIGH)
		SCL_SET(LOW); //结束当前bit传输
		udelay(1); // data hold time(t_HD;DAT)
	}

	/* wait for ACK/NAK */
	SDA_IN();
	SCL_SET(HIGH); // clk #9 for ack
	udelay(1); // data set-up time (t_SU;DAT)

	/* High level means NAK */
	if( SDA_GET() )
		error = -ERROR_ACK;

	SCL_SET(LOW);

	udelay(1);

	return error;
}

/* 检测从设备是否正在Clock Stretching, 即从设备是否拉低SCL以延长通信周期，若超时仍未释放SCL，返回错误 */
static inline int I2C_WaitWhileClockStreching(uint8_t timeout)
{
	while( SCL_GET() == 0 )
	{
		if( timeout-- == 0 )
			return -ERROR_TIMEOUT;

		udelay(10);
	}
	return ERROR_NONE;
}


/* 从 I2C 设备读取1字节数据，支持始终拉伸检测和可配置的ACK/NAK响应 */
int I2C_ReadByte(uint8_t *rxByte, uint8_t ack, uint32_t timeout)
{
	uint8_t error = 0;
	uint8_t mask;

	*rxByte = 0x00;
	SDA_IN();

	/* Start read 8 bits data, MSB */
	for(mask = 0x80; mask > 0; mask >>= 1)
	{
		SCL_SET(HIGH); // start clock on SCL-line
		udelay(1); // clock set-up time (t_SU;CLK)

		/* wait while clock streching */
		error = I2C_WaitWhileClockStreching(timeout);

		udelay(1); // SCL high time (t_HIGH)

		if( SDA_GET() )
			*rxByte |= mask; //read bit

		SCL_SET(LOW);
		udelay(1); // data hold time(t_HD;DAT)
	}

	I2C_Ack(ack);

	return error;
}


/* I2C High level API functions */

/* 锁定I2C总线供特定设备使用 */
int i2c_lock(uint8_t addr)
{
	/* I2C bus already be used */
	while( i2c_bus.addr && i2c_bus.addr != addr )
		mdelay(10);

	/* 获得总线控制权后，设置当前设备地址 */
	i2c_bus.addr = addr;

	return 0;
}

void i2c_free(void)
{
	i2c_bus.addr = 0x0;
}

int i2c_write(uint8_t *data, int bytes)
{
	int	i;
	int	rv = 0;

	if( !data )
		return -ERROR_PARM;

	/* send start condition */
	I2C_StartCondition();

	/* send device address for write */
	if( (rv = I2C_SendAddress(I2C_WR)) <0 )
		goto OUT;

	/* send data to slave device */
	for(i=0; i<bytes; i++)
	{
		if((rv = I2C_WriteByte(data[i])) <0 )
			break;
	}

OUT:
	/* send stop condition */
	I2C_StopCondition();

	return rv;
}

int i2c_read(uint8_t *buf, int size)
{
	int		i;
	int		rv = ERROR_NONE;
	uint8_t	byte;

	/* send start condition */
	I2C_StartCondition();

	/* send device address for write */
	if( (rv = I2C_SendAddress(I2C_RD)) <0 )
		goto OUT;

	/* read data from slave device */
	for(i=0; i<size; i++)
	{
		if((rv = I2C_ReadByte(&byte, ACK, I2C_CLK_STRETCH_TIMEOUT)) < 0)
			goto OUT;

		buf[i] = byte;
	}

OUT:
	/* send stop condition */
	I2C_StopCondition();

	return rv;
}
